﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Security;
using System.Text;
using System.Threading.Tasks;

namespace ExchangeHog.IMAPClient
{
	class Mailbox
	{
		String uidunique;

		int nExists;
		int nRecent;
	}

	class CommandStatus
	{
		String tag;
		String command;
		public Boolean? wasSuccessful;
		String response;

		public CommandStatus(String tag, String command)
		{
			this.tag = tag;
			this.command = command;
			wasSuccessful = null;
			response = null;
		}

		public void addResponse(String response)
		{
			if (response.StartsWith("OK"))
				wasSuccessful = true;
			else
				wasSuccessful = false;
			response = response.Substring(response.IndexOf(" ") + 1);
		}
	}

    public class IMAPClient
    {
		TcpClient client;
		NetworkStream stream;

		List<String> information = new List<String>();
		List<String> warnings = new List<String>();
		Dictionary<String, CommandStatus> mapTagToCommandStatus = new Dictionary<string,CommandStatus>();

		public void connect(String server, Int32 port)
		{
			client = new TcpClient(server, port);
			stream = client.GetStream();
			Console.WriteLine("Connected: got " + expectUntaggedOK());
		}

		public void disconnect()
		{
			client.Close();
			stream.Close();
		}

		private String read()
		{
			StringBuilder sb = new StringBuilder();
			do
			{
				Byte[] data = new Byte[256];
				Int32 bytes = stream.Read(data, 0, data.Length);
				sb.Append(System.Text.Encoding.ASCII.GetString(data, 0, bytes));
			} while (stream.DataAvailable);
			String responseData = sb.ToString();
			Console.WriteLine("Received: {0}", responseData);
			return responseData;
		}

        private String read(String terminator)
        {
            StringBuilder sb = new StringBuilder();
            char[] endofresponse = new char[10];
            Byte[] data = new Byte[256];
            while (true)
            {
                Int32 bytes = stream.Read(data, 0, data.Length);
                sb.Append(System.Text.Encoding.ASCII.GetString(data, 0, bytes));
                int posSB = sb.Length - 1;
                int posTerm = terminator.Length - 1;
                while (posTerm >= 0 && posSB >= 0 && terminator[posTerm] == sb[posSB])
                {
                    posSB--;
                    posTerm--;
                }
                if (posTerm < 0)
                    break;
            }
            String responseData = sb.ToString();
            Console.WriteLine("Received: {0}", responseData);
            return responseData;
        }

		private void send(String command)
		{
			Byte[] data = System.Text.Encoding.ASCII.GetBytes(command);
			stream.Write(data, 0, data.Length);
			Console.WriteLine("Sent: {0}", command);
		}

		private void sendLiteral(byte[] data)
		{
			stream.Write(data, 0, data.Length);
			Console.WriteLine("Sent {0} bytes", data.Length);
		}

		bool serverWaitsForContinuation;
		int theTag;
		object lockTag = new Object();
		bool ServerWaitsForContinuation
		{
			get
			{
				lock(lockTag)
				{
					return serverWaitsForContinuation;
				}
			}
			set
			{
				lock(lockTag)
				{
					serverWaitsForContinuation = value;
				}
			}
		}

		private CommandStatus sendCommand(String command)
		{
			lock (lockTag)
			{
				String sTag = String.Format("A{0:00000}", theTag++);
				serverWaitsForContinuation = false;
				send(sTag + " " + command);

				CommandStatus cs = new CommandStatus(sTag, command);
				mapTagToCommandStatus.Add(sTag, cs);
				return cs;
			}
		}

		private void processServerResponse()
		{
			String response = read("\r\n");
			String tag = null;
			if (response[0] == '*')
			{
				//
				//	untagged response
				//
				int nEndOfStatus = response.IndexOf(" ", 2);
				String status = response.Substring(2, nEndOfStatus - 2);
				String text = response.Substring(nEndOfStatus).TrimStart();
				if (status.CompareTo("OK") == 0)
					information.Add(text);
				else if (status.CompareTo("NO") == 0 || status.CompareTo("BAD") == 0)
					warnings.Add(text);
			}
			else if (response[0] == '+')
			{
				//
				//	server is ready for more data from us
				//
				ServerWaitsForContinuation = true;
			}
			else
			{
				int posFirstSpace = response.IndexOf(' ');
				tag = response.Substring(0, posFirstSpace);
				mapTagToCommandStatus[tag].addResponse(response.Substring(posFirstSpace + 1));
			}

			//
			//	EXPUNGE 
			//	FETCH <message_data>
			//		<message_data> = BODY | BODY[<section>]<<origin_octect>> | BODYSTRUCTURE ... | ENVELOPE ... | FLAGS ... | INTERNALDATE ... | RFC822 ... | RFC822.HEADER ... | RFC822.SIZE ... | RFC822.TEXT ... | UID ...
			//
		}

		private String expectUntaggedOK()
		{
			do
			{
				processServerResponse();
			} while (information.Count == 0);
			if (information.Count == 0)
				throw new Exception("Expected informational message");
			String message = information[0];
			information.RemoveAt(0);
			return message;
		}

		public void addMessage(byte[] message)
		{
			CommandStatus cs = sendCommand(String.Format("APPEND INBOX {{{0}}}\r\n", message.Length-1));
			do
			{
				processServerResponse();
			} while (!ServerWaitsForContinuation && !cs.wasSuccessful.HasValue); // wait for server to confirm its ready for actual data

			if (cs.wasSuccessful.HasValue && !cs.wasSuccessful.Value)
				throw new Exception(String.Format("Failed to add message {0}", message));

			sendLiteral(message);

			do
			{
				processServerResponse();
			} while (!cs.wasSuccessful.HasValue);
		}

		public void login(String userid, SecureString password)
		{
			CommandStatus cs = sendCommand(String.Format("LOGIN {0} {1}\r\n", userid, Util.Util.ConvertToUnsecureString(password)));
			do
			{
				processServerResponse();
			} while (!cs.wasSuccessful.HasValue);

			if (!cs.wasSuccessful.Value)
				throw new Exception("Failed to login");
		}
    }
}
